﻿using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Input;
using System.Windows;

namespace Avivo.Tools.Sprite.Models
{
	public class SpriteViewModel : ModelBase
	{
		#region Private members
		private bool enableCss = true;
		private bool useExternalProcess = false;
		private bool buildSampleHtml = true;
		private bool isStandalone = false;
		private string baseClassName;
		private string inputDirectory;
		private string outputDirectory;
		private ObservableCollection<string> templateList;
		private ObservableCollection<SpriteModel> sprites;
		private ICommand generateSpriteCommand;
		private ICommand inputBrowse;
		private ICommand outputBrowse;
		private ICommand loadXmlCommand;
		private ICommand saveXmlCommand;
		private ICommand resetCommand;
		private FileSystemWatcher fsw;
		private IContractService contractService = null;
		#endregion

		#region Constructor
		public SpriteViewModel()
		{
			//NOTE: FileSystemWatcher must be created before calling Clear() method
			fsw = new FileSystemWatcher();

			//Reset all variables
			Clear();

			//Initialize FileSystemWatcher to track file changes
			fsw.Path = this.InputDirectory;
			fsw.IncludeSubdirectories = false;
			fsw.Filter = "*.xaml";
			fsw.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime;
			fsw.Created += new FileSystemEventHandler(fsw_Created);
			fsw.Deleted += new FileSystemEventHandler(fsw_Deleted);
			fsw.Renamed += new RenamedEventHandler(fsw_Renamed);
			fsw.EnableRaisingEvents = true;
		}
		#endregion

		#region Public properties
		/// <summary>
		/// Gets or sets a value indicating whether to generate css stylesheet.
		/// </summary>
		public bool EnableCss
		{
			get
			{
				return this.enableCss;
			}
			set
			{
				if (this.enableCss != value)
				{
					this.enableCss = value;
					Settings.EnableCss = value;
					OnPropertyChanged("EnableCss");
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether to generate sample html document.
		/// </summary>
		public bool BuildSampleHtml
		{
			get
			{
				return this.buildSampleHtml;
			}
			set
			{
				if (this.buildSampleHtml != value)
				{
					this.buildSampleHtml = value;
					Settings.BuildSampleHtml = value;
					OnPropertyChanged("BuildSampleHtml");
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether to use external application to generate sprite images and join them together.
		/// </summary>
		public bool UseExternalProcess
		{
			get
			{
				return this.useExternalProcess;
			}
			set
			{
				if (this.useExternalProcess != value)
				{
					this.useExternalProcess = value;
					OnPropertyChanged("UseExternalProcess");
				}
			}
		}

		/// <summary>
		/// Gets or sets a css class name that references sprite image.
		/// </summary>
		public string BaseClassName
		{
			get
			{
				return this.baseClassName;
			}
			set
			{
				if (this.baseClassName != value)
				{
					this.baseClassName = value;
					Settings.BaseClassName = value;
					OnPropertyChanged("BaseClassName");
					OnPropertyChanged("PreviewCssContent");
				}
			}
		}

		/// <summary>
		/// Gets or sets a directory that contains xaml template files.
		/// </summary>
		public string InputDirectory
		{
			get
			{
				return this.inputDirectory;
			}
			set
			{
				if (this.inputDirectory != value)
				{
					this.inputDirectory = value;

					try
					{
						if (Directory.Exists(value))
						{
							Settings.InputDirectory = value;
							fsw.EnableRaisingEvents = false;
							fsw.Path = this.InputDirectory;
							fsw.EnableRaisingEvents = true;
							FindTemplateNames();
							OnInputDirectoryChanged();
						}
					}
					catch (Exception ex)
					{
						ReportError(ex);
					}

					OnPropertyChanged("InputDirectory");
				}
			}
		}

		/// <summary>
		/// Gets or sets a directory where the files should be generated.
		/// </summary>
		public string OutputDirectory
		{
			get
			{
				return this.outputDirectory;
			}
			set
			{
				if (this.outputDirectory != value)
				{
					this.outputDirectory = value;
					Settings.OutputDirectory = value;
					OnOutputDirectoryChanged();
					OnPropertyChanged("OutputDirectory");
				}
			}
		}

		/// <summary>
		/// Gets a css content for preview.
		/// </summary>
		public string PreviewCssContent
		{
			get
			{
				string content = string.Format(
@"a.{0} {{ background-image: url('{0}.png'); }}
a.{1} {{
  background-position: 0px 0px;
  display: block; width: 50px; height: 50px;
}}
a.{1}:hover {{
  background-position: -50px 0px;
  display: block; width: 50px; height: 50px;
}}
a.{1} .selected {{
  background-position: -100px 0px;
  display: block; width: 50px; height: 50px;
}}", this.BaseClassName, this.Sprites.FirstOrDefault().Name ?? this.BaseClassName);
				return content;
			}
		}

		/// <summary>
		/// Gets or sets a list of sprites.
		/// </summary>
		public ObservableCollection<SpriteModel> Sprites
		{
			get
			{
				return this.sprites;
			}
			set
			{
				if (this.sprites != value)
				{
					this.sprites = value;
					this.sprites.CollectionChanged += (sender, e) =>
					{
						int position = 1;
						foreach (var item in this.sprites)
						{
							//Update index number
							item.Index = position++;

							if (item.Parent == null)
							{
								//Make this ViewModel accessible from the item
								item.Parent = this;
							}
						}
						OnPropertyChanged("PreviewCssContent");
					};
					OnPropertyChanged("Sprites");
				}
			}
		}

		/// <summary>
		/// Gets a list of available templates in the input directory.
		/// </summary>
		public ObservableCollection<string> TemplateList
		{
			get
			{
				return this.templateList;
			}
			protected set
			{
				if (this.templateList != value)
				{
					this.templateList = value;
					OnPropertyChanged("TemplateList");
				}
			}
		}

		public ICommand GenerateSpriteCommand
		{
			get
			{
				return this.generateSpriteCommand;
			}
			set
			{
				if (this.generateSpriteCommand != value)
				{
					this.generateSpriteCommand = value;
					OnPropertyChanged("GenerateSpriteCommand");
				}
			}
		}

		public ICommand InputBrowseCommand
		{
			get
			{
				return this.inputBrowse;
			}
			set
			{
				if (this.inputBrowse != value)
				{
					this.inputBrowse = value;
					OnPropertyChanged("InputBrowseCommand");
				}
			}
		}

		public ICommand OutputBrowseCommand
		{
			get
			{
				return this.outputBrowse;
			}
			set
			{
				if (this.outputBrowse != value)
				{
					this.outputBrowse = value;
					OnPropertyChanged("OutputBrowseCommand");
				}
			}
		}

		public ICommand LoadXmlCommand
		{
			get
			{
				return this.loadXmlCommand;
			}
			set
			{
				if (this.loadXmlCommand != value)
				{
					this.loadXmlCommand = value;
					OnPropertyChanged("LoadXmlCommand");
				}
			}
		}

		public ICommand SaveXmlCommand
		{
			get
			{
				return this.saveXmlCommand;
			}
			set
			{
				if (this.saveXmlCommand != value)
				{
					this.saveXmlCommand = value;
					OnPropertyChanged("SaveXmlCommand");
				}
			}
		}

		public ICommand ResetCommand
		{
			get
			{
				return this.resetCommand;
			}
			set
			{
				if (this.resetCommand != value)
				{
					this.resetCommand = value;
					OnPropertyChanged("ResetCommand");
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether the module is hosted in a stand-alone application (i.e. not as Expression Blend plugin).
		/// </summary>
		public bool IsStandalone
		{
			get
			{
				return this.isStandalone;
			}
			set
			{
				if (this.isStandalone != value)
				{
					this.isStandalone = value;
					OnPropertyChanged("IsStandalone");
				}
			}
		}
		#endregion

		#region Protected methods
		/// <summary>
		/// Overloaded. Finds template names in the input directory and fills ComboBoxes with the new values.
		/// </summary>
		protected void FindTemplateNames()
		{
			FindTemplateNames(null, null);
		}

		/// <summary>
		/// Finds template names in the input directory and fills ComboBoxes with the new values.
		/// Use this method when a file renaming has been detected.
		/// </summary>
		/// <param name="oldFullPath"></param>
		/// <param name="newFullPath"></param>
		protected void FindTemplateNames(string oldFullPath, string newFullPath)
		{
			try
			{
				if (Directory.Exists(this.InputDirectory))
				{
					var list = this.TemplateList;
					var templates = new ObservableCollection<string>();

					#region Find template names
					//Find all available templates in the directory
					//var pattern = new Regex(@"([\w\d]+)_(normal|hover).xaml", RegexOptions.IgnoreCase);
					var pattern = new Regex(@"([\w\d\-]+)_([\w\d\-]+).xaml", RegexOptions.IgnoreCase);
					var files = Directory.GetFiles(this.InputDirectory, "*.xaml", SearchOption.TopDirectoryOnly);
					foreach (string file in files)
					{
						var match = pattern.Match(file);
						if (match.Success)
						{
							string template = match.Groups[1].Value.ToLower();
							if (!templates.Contains(template))
							{
								templates.Add(template);
							}
						}
					}
					#endregion

					#region Update renamed template name
					//Only if renamed - update old template name with new one
					if (oldFullPath != null && newFullPath != null)
					{
						var match = pattern.Match(Path.GetFileName(oldFullPath).ToLower());
						string oldTemplate = match.Success ? match.Groups[1].Value : null;

						match = pattern.Match(Path.GetFileName(newFullPath).ToLower());
						string newTemplate = match.Success ? match.Groups[1].Value : null;

						//If template part changed if file name and old template name does no longer exists
						if (oldTemplate != null && newTemplate != null && oldTemplate != newTemplate && !templates.Contains(oldTemplate))
						{
							foreach (var sprite in this.Sprites.Where(x => x.Template == oldTemplate))
							{
								//Update sprite with new tempate name
								sprite.Template = newTemplate;
							}
						}
					}
					#endregion

					#region Remove non-existing template items
					//For each removed template from the disk...
					foreach (var sprite in this.Sprites)
					{
						if (sprite.Template == null || !templates.Contains(sprite.Template))
						{
							//TODO: Remove or change template?!
							//this.Sprites.Remove(sprite);
							sprite.Template = null;
						}
					}
					#endregion

					#region Create new template items
					//For each removed template...
					foreach (string template in templates)
					{
						//Check if a template instance exists otherwise create a new sample instance
						var sprite = this.Sprites.FirstOrDefault(e => e.Template == template);
						if (sprite == null)
						{
							this.Sprites.Add(new SpriteModel(this) { Name = template, Template = template });
						}
					}
					#endregion

					this.TemplateList = templates;

					//Remove unused empty items
					var sprites = this.Sprites.Where(x => string.IsNullOrEmpty(x.Name) && string.IsNullOrEmpty(x.Content)).ToList();
					foreach (var item in sprites)
					{
						if (this.Sprites.Count > 1)
						{
							this.Sprites.Remove(item);
						}
					}
				}
			}
			catch (Exception ex)
			{
				ReportError(ex);
			}
		}

		/// <summary>
		/// Reports an error.
		/// </summary>
		/// <param name="error"></param>
		protected void ReportError(Exception error)
		{
			if (error != null)
			{
				WriteLine(error.Message);
			}

			if (this.IsStandalone && error != null)
			{
				MessageBox.Show(error.Message, "Error", MessageBoxButton.OK);
			}
		}

		/// <summary>
		/// Writes a message to Expression Blend's logging window.
		/// </summary>
		/// <param name="message"></param>
		protected void WriteLine(string message)
		{
			if (this.contractService != null)
			{
				this.contractService.WriteLine(message);
			}
		}
		#endregion

		#region Public methods
		/// <summary>
		/// Sets the default values.
		/// </summary>
		public void Clear()
		{
			this.TemplateList = new ObservableCollection<string>();
			this.Sprites = new ObservableCollection<SpriteModel>();
			this.Sprites.Add(new SpriteModel(this));

			this.InputDirectory = Settings.InputDirectory;
			this.OutputDirectory = Settings.OutputDirectory;
			this.BuildSampleHtml = Settings.BuildSampleHtml;
			this.BaseClassName = Settings.BaseClassName;
			this.EnableCss = Settings.EnableCss;

			this.GenerateSpriteCommand = new GenerateSpriteCommand(this);
			this.SaveXmlCommand = new SaveXmlCommand(this);
			this.LoadXmlCommand = new LoadXmlCommand(this);
			this.ResetCommand = new ResetCommand(this);
			this.InputBrowseCommand = new BrowseCommand(this) { IsBrowseForOutputDirectory = false };
			this.OutputBrowseCommand = new BrowseCommand(this) { IsBrowseForOutputDirectory = true };
		}

		/// <summary>
		/// Loads a document that has been opened in Expression Blend.
		/// </summary>
		/// <param name="contractService"></param>
		public void RegisterContractService(IContractService contractService)
		{
			this.contractService = contractService;
			this.contractService.DocumentChanged += (sender, e) =>
			{
				if (this.contractService != null && !string.IsNullOrEmpty(this.contractService.DocumentPath))
				{
					this.InputDirectory = Path.GetDirectoryName(this.contractService.DocumentPath);
				}
			};
			if (this.contractService != null && !string.IsNullOrEmpty(this.contractService.DocumentPath))
			{
				this.InputDirectory = Path.GetDirectoryName(this.contractService.DocumentPath);
			}
		}
		#endregion

		#region Event handlers
		protected delegate void CrossThreadDelegate();

		private void fsw_Created(object sender, FileSystemEventArgs e)
		{
			Application.Current.Dispatcher.BeginInvoke(new CrossThreadDelegate(() =>
			{
				FindTemplateNames();
			}));
		}

		private void fsw_Deleted(object sender, FileSystemEventArgs e)
		{
			Application.Current.Dispatcher.BeginInvoke(new CrossThreadDelegate(() =>
			{
				FindTemplateNames();
			}));
		}

		private void fsw_Renamed(object sender, RenamedEventArgs e)
		{
			Application.Current.Dispatcher.BeginInvoke(new CrossThreadDelegate(() =>
			{
				FindTemplateNames(e.OldFullPath, e.FullPath);
			}));
		}
		#endregion

		#region Bubble events
		public event EventHandler InputDirectoryChanged;
		protected virtual void OnInputDirectoryChanged()
		{
			if (InputDirectoryChanged != null)
			{
				InputDirectoryChanged(this, new EventArgs());
			}
		}

		public event EventHandler OutputDirectoryChanged;
		protected virtual void OnOutputDirectoryChanged()
		{
			if (OutputDirectoryChanged != null)
			{
				OutputDirectoryChanged(this, new EventArgs());
			}
		}
		#endregion
	}
}
